<?php
/**
 * Created by PhpStorm.
 * User: kejing.song
 * Email: 597481334@qq.com
 * Date: 2015/7/3
 * Time: 11:40
 */

namespace ClassLibrary;

use think\Exception;

/**
 * Class ClHttp 类库Http
 * @package Common\Common
 */
class ClHttp {

    /**
     * 获取协议
     * @return array|string|null
     * @date 2023/9/25 23:37
     */
    public static function getProtocol() {
        if (!request()) {
            return '';
        }
        $protocol = request()->header('x-forwarded-proto');
        if (empty($protocol)) {
            $protocol = 'http';
        }
        $protocol .= '://';
        return $protocol;
    }

    /**
     * 获取服务器地址
     * @param bool $with_protocol
     * @return string
     */
    public static function getServerDomain($with_protocol = true) {
        $protocol = '';
        if (is_null(request())) {
            return '';
        }
        if ($with_protocol) {
            $protocol = self::getProtocol();
        }
        $domain = request()->header('x-forwarded-server', null);
        if (!empty($domain)) {
            return $protocol . $domain;
        }
        $domain = request()->header('x-forwarded-host', null);
        if (!empty($domain)) {
            return $protocol . $domain;
        }
        return $protocol . request()->header('host');
    }

    /**
     * 获取当前请求路径
     * @return string
     */
    public static function getCurrentRequest() {
        if (!request()) {
            return '';
        }
        return self::getServerDomain() . request()->uri();
    }

    /**
     * 非阻塞请求
     * Nginx应做如下配置修改：
     * fastcgi_send_timeout 30000; #nginx进程向fastcgi进程发送request的整个过程的超时时间
     * fastcgi_read_timeout 30000; #fastcgi进程向nginx进程发送response的整个过程的超时时间
     * proxy_ignore_client_abort on; #不允许代理端主动关闭连接
     * @param string $ip 服务器ip地址 192.168.1.168
     * @param string $url 请求url http://www.sda1.test/Index/delay
     * @param array $params 参数数组 array('name' => '张三')
     * @param int $timeout 超时时间
     * @return bool
     */
    public static function noBlockingRequest($ip, $url, $params = array(), $timeout = 15) {
        if (config('app_debug')) {
            log_info($ip, $url, $params);
        }
        $matches = parse_url($url);
        !isset($matches['host']) && $matches['host'] = '';
        !isset($matches['path']) && $matches['path'] = '';
        !isset($matches['query']) && $matches['query'] = '';
        !isset($matches['port']) && $matches['port'] = '';
        $host   = $matches['host'];
        $path   = $matches['path'] ? $matches['path'] . ($matches['query'] ? '?' . $matches['query'] : '') : '/';
        $port   = !empty($matches['port']) ? $matches['port'] : 80;
        $cookie = '';
        $post   = '';
        if (is_array($params) && count($params) > 0) {
            foreach ($params as $name => $value) {
                if (empty($post)) {
                    $post .= $name . '=' . $value;
                } else {
                    $post .= '&' . $name . '=' . $value;
                }
            }
        }
        $user_agent = request()->header('user-agent');
        if ($post) {
            if (is_array($post)) {
                $post = http_build_query($post);
            }
            $out = "POST $path HTTP/1.0\r\n";
            $out .= "Accept: */*\r\n";
            $out .= "Accept-Language: zh-cn\r\n";
            $out .= "Content-Type: application/x-www-form-urlencoded\r\n";
            $out .= "User-Agent: $user_agent\r\n";
            $out .= "Host: $host\r\n";
            $out .= 'Content-Length: ' . strlen($post) . "\r\n";
            $out .= "Connection: Close\r\n";
            $out .= "Cache-Control: no-cache\r\n";
            $out .= "Cookie: $cookie\r\n\r\n";
            $out .= $post;
        } else {
            $out = "GET $path HTTP/1.0\r\n";
            $out .= "Accept: */*\r\n";
            $out .= "Accept-Language: zh-cn\r\n";
            $out .= "User-Agent: $user_agent\r\n";
            $out .= "Host: $host\r\n";
            $out .= "Connection: Close\r\n";
            $out .= "Cookie: $cookie\r\n\r\n";
        }
        $fp = @fsockopen(($ip ? $ip : $host), $port, $err_no, $err_str, $timeout);
        //错误
        if (empty($fp)) {
            log_info('error:', array(
                'ip:'      => $ip,
                'params:'  => $params,
                'timeout:' => $timeout,
                'err_no:'  => $err_no,
                'err_str:' => $err_str
            ));
            return false;
        }
        @stream_set_blocking($fp, 0);
        fwrite($fp, $out);
        fclose($fp);
        unset($fp);
        return true;
    }

    /**
     * 请求返回值为json
     */
    const REQUEST_RESULT_TYPE_JSON = 'json';

    /**
     * 请求返回值为xml
     */
    const REQUEST_RESULT_TYPE_XML = 'xml';

    /**
     * https请求
     * @param string $url 请求地址
     * @param array $params 上传文件采用 @文件绝对地址方式，如果为空，则采用GET方式提交，如果不为空，则采用POST方式提交
     * @param string $result_type REQUEST_RESULT_TYPE_JSON/REQUEST_RESULT_TYPE_XML 默认为空，不进行处理
     * @param bool $debug 是否调试
     * @param int $duration 缓存时间
     * @param array $header ["Authorization:628906641320240605145515838","Content-Type: application/json;charset=utf8"]
     * @param string $refer 来源
     * @param int $timeout 超时时间
     * @param string $proxy_ip 代理ip
     * @param string $proxy_port 代理端口
     * @param string $proxy_user_password 代理用户:代理密码
     * @return bool|mixed|string
     * @throws \Psr\SimpleCache\InvalidArgumentException
     * @throws \throwable
     * @date 2023/12/29 17:33
     */
    public static function request($url, $params = [], $result_type = '', $duration = 0, $debug = false, $header = [], $refer = '', $timeout = 30, $proxy_ip = '', $proxy_port = '', $proxy_user_password = '') {
        if ($duration > 0) {
            $key    = ClCache::getKey($url, $params);
            $result = cache($key);
            if ($result !== false) {
                if (strtolower($result_type) == self::REQUEST_RESULT_TYPE_JSON) {
                    $result = json_decode($result, true);
                } else if (strtolower($result_type) == self::REQUEST_RESULT_TYPE_XML) {
                    $result = ClXml::toArray($result);
                }
                return $result;
            }
        }
        $ch = curl_init();
        //处理包含文件的参数
        $is_post_file = false;
        if (is_array($params)) {
            foreach ($params as $param_index => $param_value) {
                if (!is_string($param_value)) {
                    continue;
                }
                if (strpos($param_value, '@') === 0) {
                    $is_post_file = true;
                    $param_value  = substr($param_value, 1);
                    clearstatcache();
                    //如果存在文件
                    if (is_file($param_value)) {
                        //兼容5.0-5.6版本的curl
                        if (class_exists('\CURLFile')) {
                            $params[$param_index] = new \CURLFile(realpath($param_value), mime_content_type($param_value), ClFile::getName($param_value, true));
                        } else {
                            if (defined('CURLOPT_SAFE_UPLOAD')) {
                                curl_setopt($ch, CURLOPT_SAFE_UPLOAD, FALSE);
                            }
                        }
                    }
                }
            }
        }
        if (strpos($url, 'https') === 0) {
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 跳过证书检查
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);  // 从证书中检查SSL加密算法是否存在
        }
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
        if (!empty($params)) {
            //json格式特殊处理
            if (!empty($header) && is_array($params)) {
                foreach ($header as $each) {
                    if (strpos($each, 'application/json') !== false) {
                        $params = json_encode($params, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
                        break;
                    }
                }
            }
            curl_setopt($ch, CURLOPT_POST, 1);
            //文件上传不可build
            curl_setopt($ch, CURLOPT_POSTFIELDS, $is_post_file ? $params : (is_array($params) ? http_build_query($params) : $params));
        }
        if (!empty($refer)) {
            curl_setopt($ch, CURLOPT_REFERER, $refer);
        }
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
        //设置代理
        if (!empty($proxy_ip) && !empty($proxy_port)) {
            curl_setopt($ch, CURLOPT_PROXY, $proxy_ip);
            curl_setopt($ch, CURLOPT_PROXYPORT, $proxy_port);
            if (!empty($proxy_user_password)) {
                curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxy_user_password);
            }
        }
        //浏览器
        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36 Core/1.47.640.400 QQBrowser/9.4.8309.400');
        if ($debug) {
            log_info('url:', $url);
            log_info('header:', json_encode($header, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
            log_info('params:', is_array($params) ? json_encode($params, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : $params);
        }
        $response = curl_exec($ch);
        if ($error = curl_error($ch)) {
            // 打印错误信息
            log_info('Error', curl_error($ch));
            //关闭
            curl_close($ch);
            return [];
        }
        curl_close($ch);
        if ($duration > 0) {
            $key = ClCache::getKey($url, $params);
            //缓存
            cache($key, $response, $duration);
        }
        if ($debug) {
            //打印原始值
            log_info('$response:', $response);
        }
        if (strtolower($result_type) == self::REQUEST_RESULT_TYPE_JSON) {
            $response = json_decode($response, true);
        } else if (strtolower($result_type) == self::REQUEST_RESULT_TYPE_XML) {
            $response = ClXml::toArray($response);
        }
        return $response;
    }

    /**
     * 获取服务器根目录
     * @return mixed
     */
    public static function getServerDocumentRoot() {
        return public_path(false);
    }

    /**
     * 模拟登录
     * @param $url
     * @param string $cookie_create_key 生成唯一cookie文件的key值
     * @param $params
     * @param bool $is_post 是否是post请求
     * @return mixed
     */
    public static function simulateLogin($url, $cookie_create_key, $params, $is_post = true) {
        $curl = curl_init();//初始化curl模块
        curl_setopt($curl, CURLOPT_URL, $url);//登录提交的地址
        curl_setopt($curl, CURLOPT_HEADER, 0);//是否显示头信息
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);//是否自动显示返回的信息
        curl_setopt($curl, CURLOPT_COOKIEJAR, self::simulateGetCookieFile($cookie_create_key)); //设置Cookie信息保存在指定的文件中
        curl_setopt($curl, CURLOPT_POST, ($is_post) ? 1 : 0);//post方式提交
        curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($params));//要提交的信息
        //浏览器
        curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36 Core/1.47.640.400 QQBrowser/9.4.8309.400');
        $rs = curl_exec($curl);//执行cURL
        curl_close($curl);//关闭cURL资源，并且释放系统资源
        return $rs;
    }

    /**
     * 模拟http请求
     * @param $url
     * @param string $cookie 生成唯一cookie文件的key值或者是coockie内容
     * @param $params
     * @param bool $is_post
     * @param string $referer
     * @param bool $is_debug
     * @return mixed
     */
    public static function simulateRequest($url, $cookie, $params, $is_post = true, $referer = '', $is_debug = false) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        clearstatcache();
        if (is_file($cookie)) {
            curl_setopt($ch, CURLOPT_COOKIEFILE, self::simulateGetCookieFile($cookie)); //读取cookie
        } else {
            curl_setopt($ch, CURLOPT_COOKIE, $cookie);
        }
        if (!empty($referer)) {
            curl_setopt($ch, CURLOPT_REFERER, $referer);
        }
        curl_setopt($ch, CURLOPT_POST, ($is_post) ? 1 : 0);//post方式提交
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));//要提交的信息
        //浏览器
        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36 Core/1.47.640.400 QQBrowser/9.4.8309.400');
        $rs = curl_exec($ch); //执行cURL抓取页面内容
        curl_close($ch);
        if ($is_debug) {
            log_info([
                'url'     => $url,
                'cookie'  => $cookie,
                'params'  => $params,
                'is_post' => $is_post,
                'result'  => $rs
            ]);
        }
        return $rs;
    }

    /**
     * 获取cookie文件
     * @param $cookie_create_key
     * @return string
     */
    private static function simulateGetCookieFile($cookie_create_key) {
        $cookie_file = public_path(false) . '/Cookie/' . $cookie_create_key . '.cookie';
        clearstatcache();
        if (!is_file($cookie_file)) {
            //创建文件夹
            ClFile::dirCreate($cookie_file);
            //创建文件
            $f = fopen($cookie_file, 'w+');
            fclose($f);
        }
        return $cookie_file;
    }

    /**
     * 获取http所有的状态码
     * @return array
     */
    public static function getHttpStatusAllCodes() {
        return array(
            #-------------------------------------
            # Define 100 series http codes (informational)
            #-------------------------------------
            100 => '100 Continue',
            101 => '101 Switching Protocols',

            #-------------------------------------
            # Define 200 series http codes (successful)
            #-------------------------------------
            200 => '200 OK',
            201 => '201 Created',
            202 => '202 Accepted',
            203 => '203 Non-Authoritative Information',
            204 => '204 No Content',
            205 => '205 Reset Content',
            206 => '206 Partial Content',

            #-------------------------------------
            # Define 300 series http codes (redirection)
            #-------------------------------------
            300 => '300 Multiple Choices',
            301 => '301 Moved Permanently',
            302 => '302 Found',
            303 => '303 See Other',
            304 => '304 Not Modified',
            305 => '305 Use Proxy',
            306 => '306 (Unused)',
            307 => '307 Temporary Redirect',

            #-------------------------------------
            # Define 400 series http codes (client error)
            #-------------------------------------
            400 => '400 Bad Request',
            401 => '401 Unauthorized',
            402 => '402 Payment Required',
            403 => '403 Forbidden',
            404 => '404 Not Found',
            405 => '405 Method Not Allowed',
            406 => '406 Not Acceptable',
            407 => '407 Proxy Authentication Required',
            408 => '408 Request Timeout',
            409 => '409 Conflict',
            410 => '410 Gone',
            411 => '411 Length Required',
            412 => '412 Precondition Failed',
            413 => '413 Request Entity Too Large',
            414 => '414 Request-URI Too Long',
            415 => '415 Unsupported Media Type',
            416 => '416 Requested Range Not Satisfiable',
            417 => '417 Expectation Failed',

            #-------------------------------------
            # Define 500 series http codes (server error)
            #-------------------------------------
            500 => '500 Internal Server Error',
            501 => '501 Not Implemented',
            502 => '502 Bad Gateway',
            503 => '503 Service Unavailable',
            504 => '504 Gateway Timeout',
            505 => '505 HTTP Version Not Supported'
        );
    }

    /**
     * 按code获取内容
     * @param $code
     * @return string
     */
    public static function getHttpStatusByCode($code) {
        $code = intval($code);
        if (empty($code)) {
            return '';
        }
        $codes = self::getHttpStatusAllCodes();
        return isset($codes[$code]) ? $codes[$code] : '';
    }

    /**
     * 伪造ip请求
     * @param $ip
     * @param $port
     * @param $url
     * @return mixed
     */
    public static function httpFake($ip, $port, $url) {
        $ch      = curl_init();
        $timeout = 5;
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
        curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC); //代理认证模式
        curl_setopt($ch, CURLOPT_PROXY, $ip); //代理服务器地址
        curl_setopt($ch, CURLOPT_PROXYPORT, $port); //代理服务器端口
        //curl_setopt($ch, CURLOPT_PROXYUSERPWD, ":"); //http代理认证帐号，username:password的格式
        curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); //使用http代理模式
        $file_contents = curl_exec($ch);
        curl_close($ch);
        return $file_contents;
    }

    /**
     * 使用 fsockopen 通过 HTTP 协议直接访问(采集)远程文件
     * 如果主机或服务器没有开启 CURL 扩展可考虑使用
     * fsockopen 比 CURL 稍慢,但性能稳定
     * @param string $url 远程URL
     * @param array $post 数组形式
     * @param string $cookie 携带cookie访问,该参数是cookie内容
     * @param string $content_type header type
     * @param int $timeout 采集超时时间
     * @param int $limit 分段读取字符个数
     * @param bool $block 是否阻塞访问,默认为true
     * @return string
     */
    public static function fsockopenDownload($url, $post = [], $cookie = '', $content_type = 'application/x-www-form-urlencoded', $timeout = 15, $limit = 0, $block = true) {
        $return  = '';
        $matches = parse_url($url);
        !isset($matches['host']) && $matches['host'] = '';
        !isset($matches['path']) && $matches['path'] = '';
        !isset($matches['query']) && $matches['query'] = '';
        !isset($matches['port']) && $matches['port'] = '';
        $host       = $matches['host'];
        $path       = $matches['path'] ? $matches['path'] . ($matches['query'] ? '?' . $matches['query'] : '') : '/';
        $port       = !empty($matches['port']) ? $matches['port'] : 80;
        $referer    = $matches['scheme'] . '://' . $matches['host'];
        $user_agent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36 Core/1.47.640.400 QQBrowser/9.4.8309.400';
        if (!empty(request()->header('user-agent'))) {
            $user_agent = request()->header('user-agent');
        }
        if ($post) {
            if (is_array($post)) {
                $post = http_build_query($post);
            }
            $out = "POST $path HTTP/1.0\r\n";
            $out .= "Accept: */*\r\n";
            //$out .= "Referer: $boardurl\r\n";
            $out .= "Accept-Language: zh-cn\r\n";
            $out .= "Content-Type: $content_type\r\n";
            $out .= "User-Agent: $user_agent\r\n";
            $out .= "Host: $host\r\n";
            $out .= 'Content-Length: ' . strlen($post) . "\r\n";
            $out .= "Connection: Close\r\n";
            $out .= "Cache-Control: no-cache\r\n";
            $out .= "Cookie: $cookie\r\n";
            $out .= "Origin: $referer\r\n";
            $out .= "Referer: $referer\r\n\r\n";
            $out .= $post;
        } else {
            $out = "GET $path HTTP/1.0\r\n";
            $out .= "Accept: */*\r\n";
            //$out .= "Referer: $boardurl\r\n";
            $out .= "Accept-Language: zh-cn\r\n";
            $out .= "User-Agent: $user_agent\r\n";
            $out .= "Host: $host\r\n";
            $out .= "Connection: Close\r\n";
            $out .= "Cookie: $cookie\r\n";
            $out .= "Origin: $referer\r\n";
            $out .= "Referer: $referer\r\n\r\n";
        }
        $fp = @fsockopen($host, $port, $errno, $errstr, $timeout);
        if (!$fp) {
            return '';
        } else {
            stream_set_blocking($fp, $block);
            stream_set_timeout($fp, $timeout);
            @fwrite($fp, $out);
            $status = stream_get_meta_data($fp);
            if (!$status['timed_out']) {
                while (!feof($fp)) {
                    if (($header = @fgets($fp)) && ($header == "\r\n" || $header == "\n")) {
                        break;
                    }
                }
                $stop = false;
                while (!feof($fp) && !$stop) {
                    $data   = fread($fp, ($limit == 0 || $limit > 8192 ? 8192 : $limit));
                    $return .= $data;
                    if ($limit) {
                        $limit -= strlen($data);
                        $stop  = $limit <= 0;
                    }
                }
            }
            @fclose($fp);
            return $return;
        }
    }

    /**
     * 分析返回用户网页浏览器名称
     * @param string $user_agent
     * @return array 返回的数组第一个为浏览器名称，第二个是版本号。
     */
    public static function getBrowser($user_agent = '') {
        $sys = empty($user_agent) ? request()->header('user-agent') : $user_agent;
        $exp = array();
        if (stripos($sys, 'MQQBrowser')) {
            $exp[0] = "MQQBrowser";
            $exp[1] = "";
        } elseif (stripos($sys, 'QQBrowser')) {
            $exp[0] = "QQBrowser";
            $exp[1] = "";
        } elseif (stripos($sys, "Opera") > 0 || stripos($sys, "OPR/") > 0) {
            $exp[0] = "Opera";
            $exp[1] = "";
        } elseif (stripos($sys, 'Edge')) {
            $exp[0] = "Edge";
            $exp[1] = '';
        } else if (stripos($sys, "Safari/") > 0) {
            $exp[0] = "Safari";
            $exp[1] = "";
        } else if (stripos($sys, "NetCaptor") > 0) {
            $exp[0] = "NetCaptor";
            $exp[1] = "";
        } elseif (stripos($sys, "Firefox/") > 0) {
            preg_match('/Firefox\/([^;)]+)+/i', $sys, $b);
            $exp[0] = 'Mozilla Firefox';
            $exp[1] = $b[1];
        } elseif (stripos($sys, "MAXTHON") > 0) {
            preg_match('/MAXTHON\s+([^;)]+)+/i', $sys, $b);
            preg_match('/MSIE\s+([^;)]+)+/i', $sys, $ie);
            // $exp = $b[0]." (IE".$ie[1].")";
            if (empty($b) || empty($ie)) {
                $exp[0] = "未知浏览器";
                $exp[1] = "";
            } else {
                $exp[0] = $b[0] . " (IE" . $ie[1] . ")";
                $exp[1] = $ie[1];
            }
        } elseif (stripos($sys, "MSIE") > 0) {
            preg_match('/MSIE([^;)]+)+/i', $sys, $ie);
            //$exp = "Internet Explorer ".$ie[1];
            //dump($sys);
            if (empty($ie)) {
                var_dump($user_agent, $ie);
            }
            $exp[0] = "Internet Explorer";
            $exp[1] = $ie[1];
        } elseif (stripos($sys, "Netscape") > 0) {
            $exp[0] = "Netscape";
            $exp[1] = "";
        } elseif (stripos($sys, "Chrome") > 0) {
            $exp[0] = "Chrome";
            $exp[1] = "";
        } elseif (stripos($sys, "rv:11.0") > 0) {
            //ie 11识别
            $exp[0] = "Internet Explorer";
            $exp[1] = 11;
        } else {
            $exp[0] = "未知浏览器";
            $exp[1] = "";
        }
        $exp[1] = trim($exp[1]);
        return $exp;
    }

    /**
     * 获取ip地址
     * @return mixed|string
     * @author SongKeJing qq:597481334 mobile:159-5107-8050
     * @date 2022/10/4 22:15
     */
    public static function getIp() {
        if (!request()) {
            return '';
        }
        $ip = request()->getRealIp();
        if (strpos($ip, ',') !== false) {
            //去除所有空格
            $ip  = \ClassLibrary\ClString::spaceTrim($ip);
            $ips = explode(',', $ip);
            $ip  = end($ips);
        }
        return $ip;
    }

}
